Skip to content

feat(spec,platform-objects): action param 'visible' predicate; gate create-user phone on features.phoneNumber#2871

Merged
os-zhuang merged 1 commit into
mainfrom
fix/action-param-visible-phone
Jul 13, 2026
Merged

feat(spec,platform-objects): action param 'visible' predicate; gate create-user phone on features.phoneNumber#2871
os-zhuang merged 1 commit into
mainfrom
fix/action-param-visible-phone

Conversation

@os-zhuang

@os-zhuang os-zhuang commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

What

The create-user form offers a Phone Number field that the default backend rejects — {"code":"invalid_request","message":"Phone numbers require the phoneNumber auth plugin (auth.plugins.phoneNumber)"} — because phoneNumber is an opt-in better-auth plugin (off by default), yet the sys_user create_user action statically advertises the param. This makes the form follow the plugin: no phone field unless the plugin is loaded.

How

  • ActionParamSchema (spec/ui/action.zod.ts) gains an optional visible predicate (ExpressionInputSchema / CEL), evaluated against the same scope as action-level visible (current_user / app / data / features). A dialog that honors it omits the param when it's false.
  • sys_user.create_user (platform-objects/identity/sys-user.object.ts) gates its phoneNumber param on features.phoneNumber == true. features.phoneNumber is already served in /api/v1/auth/config (getPublicConfig), so the client can evaluate it.

Back-compat: visible is optional; params without it are unaffected.

Verified

  • ActionParamSchema.safeParse({...visible}) parses + preserves the predicate; a param without visible still parses.
  • Paired objectui change (ActionParamDialog honoring param.visible) is unit-tested (6 tests: feature-off hides, feature-on shows, absent-flag hides, malformed fails open, and the normalized {dialect,source} served form) with a browser regression showing the create-user dialog renders cleanly.

Pairs with

objectui objectstack-ai/objectui#2406ActionParamDialog evaluates param.visible. Both must land for the phone field to hide (spec adds the field; objectui honors it).

Alternative considered — default-loading the phoneNumber plugin — was rejected: it turns an opt-in auth/sign-in surface (with an SMS-provider dependency for OTP) on for every deployment. "Form follows plugin" is the conservative fix.

🤖 Generated with Claude Code

…reate-user phone on features.phoneNumber

ActionParamSchema gains an optional 'visible' (ExpressionInputSchema / CEL),
evaluated against the same scope as action 'visible'. The sys_user create_user
action's phoneNumber param is gated on features.phoneNumber == true, so the form
only offers a phone field when the opt-in phoneNumber auth plugin is loaded —
otherwise admin/create-user rejects the phone. Pairs with objectui's
ActionParamDialog honoring param.visible (objectstack-ai/objectui#<pr>).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@vercel

vercel Bot commented Jul 13, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
spec Ready Ready Preview, Comment Jul 13, 2026 3:33am

Request Review

@github-actions github-actions Bot added documentation Improvements or additions to documentation protocol:ui tooling size/s labels Jul 13, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/platform-objects, @objectstack/spec.

96 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/ai/agents.mdx (via @objectstack/spec)
  • content/docs/ai/skills-reference.mdx (via @objectstack/spec)
  • content/docs/ai/skills.mdx (via @objectstack/spec)
  • content/docs/api/client-sdk.mdx (via @objectstack/spec)
  • content/docs/api/environment-routing.mdx (via @objectstack/spec)
  • content/docs/api/error-catalog.mdx (via @objectstack/spec)
  • content/docs/api/error-handling-client.mdx (via @objectstack/spec)
  • content/docs/api/error-handling-server.mdx (via @objectstack/spec)
  • content/docs/api/index.mdx (via @objectstack/spec)
  • content/docs/automation/approvals.mdx (via packages/spec)
  • content/docs/automation/flows.mdx (via @objectstack/spec)
  • content/docs/automation/hook-bodies.mdx (via packages/spec)
  • content/docs/automation/hooks.mdx (via @objectstack/spec)
  • content/docs/automation/index.mdx (via @objectstack/spec)
  • content/docs/automation/webhooks.mdx (via @objectstack/spec)
  • content/docs/automation/workflows.mdx (via @objectstack/spec)
  • content/docs/concepts/architecture.mdx (via @objectstack/spec)
  • content/docs/concepts/design-principles.mdx (via packages/spec)
  • content/docs/concepts/index.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-driven.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-lifecycle.mdx (via packages/spec)
  • content/docs/concepts/north-star.mdx (via packages/spec)
  • content/docs/data-modeling/analytics.mdx (via @objectstack/spec)
  • content/docs/data-modeling/drivers.mdx (via @objectstack/spec)
  • content/docs/data-modeling/external-datasources.mdx (via @objectstack/spec)
  • content/docs/data-modeling/field-types.mdx (via @objectstack/spec)
  • content/docs/data-modeling/fields.mdx (via @objectstack/spec)
  • content/docs/data-modeling/formulas.mdx (via @objectstack/spec)
  • content/docs/data-modeling/index.mdx (via @objectstack/spec)
  • content/docs/data-modeling/objects.mdx (via @objectstack/spec)
  • content/docs/data-modeling/queries.mdx (via @objectstack/spec)
  • content/docs/data-modeling/schema-design.mdx (via @objectstack/spec)
  • content/docs/data-modeling/seed-data.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation-rules.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation.mdx (via @objectstack/spec)
  • content/docs/deployment/troubleshooting.mdx (via @objectstack/spec)
  • content/docs/getting-started/build-with-claude-code.mdx (via @objectstack/spec)
  • content/docs/getting-started/cli.mdx (via @objectstack/spec)
  • content/docs/getting-started/common-patterns.mdx (via @objectstack/spec)
  • content/docs/getting-started/examples.mdx (via @objectstack/spec)
  • content/docs/getting-started/quick-reference.mdx (via @objectstack/spec)
  • content/docs/getting-started/quick-start.mdx (via @objectstack/spec)
  • content/docs/getting-started/validating-metadata.mdx (via @objectstack/spec)
  • content/docs/kernel/cluster.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/auth-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/cache-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/data-engine.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/index.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/metadata-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/storage-service.mdx (via packages/spec)
  • content/docs/kernel/index.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/email-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/index.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/queue-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/sharing-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/sms-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/storage-service.mdx (via packages/spec)
  • content/docs/kernel/services-checklist.mdx (via @objectstack/spec)
  • content/docs/permissions/authorization.mdx (via @objectstack/spec)
  • content/docs/permissions/permission-sets.mdx (via @objectstack/spec)
  • content/docs/permissions/permissions-matrix.mdx (via @objectstack/spec)
  • content/docs/permissions/positions.mdx (via @objectstack/spec)
  • content/docs/permissions/sharing-rules.mdx (via @objectstack/spec)
  • content/docs/plugins/adding-a-metadata-type.mdx (via @objectstack/spec)
  • content/docs/plugins/development.mdx (via @objectstack/spec)
  • content/docs/plugins/index.mdx (via @objectstack/spec)
  • content/docs/plugins/packages.mdx (via @objectstack/platform-objects, @objectstack/spec)
  • content/docs/protocol/backward-compatibility.mdx (via @objectstack/spec)
  • content/docs/protocol/diagram.mdx (via packages/spec)
  • content/docs/protocol/knowledge.mdx (via @objectstack/spec)
  • content/docs/protocol/objectos/config-resolution.mdx (via @objectstack/spec)
  • content/docs/protocol/objectos/i18n-standard.mdx (via @objectstack/spec)
  • content/docs/protocol/objectos/lifecycle.mdx (via @objectstack/spec)
  • content/docs/protocol/objectos/plugin-spec.mdx (via @objectstack/spec)
  • content/docs/protocol/objectos/runtime-capabilities.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/index.mdx (via packages/spec)
  • content/docs/protocol/objectql/query-syntax.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/schema.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/security.mdx (via packages/spec)
  • content/docs/protocol/objectql/state-machine.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/actions.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/concept.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/index.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/layout-dsl.mdx (via packages/spec)
  • content/docs/protocol/objectui/record-alert.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/widget-contract.mdx (via @objectstack/spec)
  • content/docs/releases/implementation-status.mdx (via @objectstack/spec)
  • content/docs/releases/index.mdx (via @objectstack/spec)
  • content/docs/releases/v12.mdx (via @objectstack/spec)
  • content/docs/releases/v13.mdx (via @objectstack/spec)
  • content/docs/releases/v9.mdx (via @objectstack/spec)
  • content/docs/ui/create-vs-edit-form.mdx (via @objectstack/spec)
  • content/docs/ui/dashboards.mdx (via @objectstack/spec)
  • content/docs/ui/forms.mdx (via @objectstack/spec)
  • content/docs/ui/index.mdx (via @objectstack/spec)
  • content/docs/ui/setup-app.mdx (via @objectstack/platform-objects, @objectstack/spec)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

@os-zhuang os-zhuang merged commit 609cb13 into main Jul 13, 2026
17 checks passed
@os-zhuang os-zhuang deleted the fix/action-param-visible-phone branch July 13, 2026 04:38
os-zhuang pushed a commit that referenced this pull request Jul 15, 2026
…params (#2874)

Also lists the param-level visible predicate (added in #2871) in the
ActionParam field summary, which had drifted.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HGofzwNXvtahMGk8TQqXdx
os-zhuang added a commit that referenced this pull request Jul 15, 2026
#2874) (#2965)

* feat(spec,auth): add public auth feature-flag classification registry + drift guard (#2874 P0)

Single registry (PUBLIC_AUTH_FEATURES) classifying all 13 boolean flags
served by getPublicConfig() at /auth/config: consumption surface
(crud/login/status), default semantics (opt-in '== true' vs default-on
'!= false'), and gated spec inputs or an exemption reason. Includes
featureGatePredicate + lowerRequiresFeature helpers for the upcoming
requiresFeature sugar (P1).

The plugin-auth drift guard derives the ACTUAL served key set from a real
AuthManager and asserts it equals the registry keys, so a new flag that
ships unclassified turns CI red. A semantics check also asserts each
flag's registered default matches what getPublicConfig() actually serves
- it already caught oidcProvider being default-ON (follows the MCP
surface), which issue #2874's own table misclassified as opt-in.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HGofzwNXvtahMGk8TQqXdx

* feat(spec): declarative requiresFeature sugar on actions/params, lowered to visible CEL (#2874 P1)

ActionSchema and ActionParamSchema gain an optional
requiresFeature: <flag> (z.enum over the registry keys, so a typo fails
the parse). A .transform() appended after the schemas' refinements lowers
it at parse time into the canonical visible predicate -
features.X == true for opt-in flags, features.X != false for default-on
(per PUBLIC_AUTH_FEATURES semantics) - AND-composing with any explicit
visible ((existing) && gate) and stripping the sugar key from the output,
mirroring the normalizeVisibleWhen pattern (ADR-0089). A non-CEL or
AST-only visible rejects loudly (ADR-0078) instead of silently dropping
the gate.

Renderers, lint, and objectui only ever see the canonical visible
envelope - the objectui rendering chain needs zero changes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HGofzwNXvtahMGk8TQqXdx

* refactor(platform-objects): migrate all 22 hand-written features.* gates to requiresFeature (#2874 P2a)

Mechanical migration of every visible: 'features.X ...' predicate in the
identity objects to the declarative requiresFeature annotation, including
the compound transfer_ownership gate (residual row predicate stays
hand-written; the lowering AND-composes the feature gate onto it).

A behavior-equivalence matrix in platform-objects.test.ts pins each
lowered visible.source to the exact CEL string that was previously
hand-written (parenthesized-composition for the compound row), and
asserts the sugar key never survives into parsed objects - so the
migration is provably behavior-neutral.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HGofzwNXvtahMGk8TQqXdx

* feat(spec,platform-objects): gate the audited admin/twoFactor/oidcProvider surfaces + bidirectional completeness guard (#2874 P2b)

Audit fixes - capability-dependent actions that previously rendered with
the backing plugin off and then 404'd:
- requiresFeature: 'admin' on the six sys_user platform-admin actions
  (ban/unban/unlock/set_user_password/set_user_role/impersonate_user);
  the stale 'still render but server returns 404' comment is updated.
  SCIM deployments keep them (SCIM forces features.admin on, ADR-0071).
- requiresFeature: 'twoFactor' on the three sys_two_factor actions and
  the three sys_user self-service 2FA actions (composed onto their
  existing record.id == ctx.user.id predicates).
- requiresFeature: 'oidcProvider' on the five sys_oauth_application
  actions (composed onto the enable/disable record predicates).

feature-gate-guard.test.ts asserts registry<->objects lockstep in both
directions: every gatedInputs path resolves to a real input carrying the
flag's lowered predicate (removing an annotation goes red), and every
features.* reference in a visible predicate is booked in the registry
(adding a gate without bookkeeping goes red).

Login-surface audit gaps are recorded in the registry and tracked in
objectui#2513 (DeviceAuthPage unguarded) and objectui#2514
(passkeys/magicLink advertised but unconsumed).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HGofzwNXvtahMGk8TQqXdx

* chore: changeset for the feature-gate registry + requiresFeature work (#2874)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HGofzwNXvtahMGk8TQqXdx

* docs(protocol): document requiresFeature capability gates on actions/params (#2874)

Also lists the param-level visible predicate (added in #2871) in the
ActionParam field summary, which had drifted.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HGofzwNXvtahMGk8TQqXdx

* fix(ci): classify action/requiresFeature in the liveness ledger; avoid reserved word in docs example

- Spec property liveness ratchet: requiresFeature is live at parse time
  (lowered into the already-live visible predicate and stripped), with
  evidence pointing at lowerRequiresFeature and the #2874 guard tests.
- check-role-word (ADR-0090 D3): swap the docs composition example from
  transfer_ownership to disable_oauth_application so no new use of the
  reserved word ships.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HGofzwNXvtahMGk8TQqXdx

* fix(ci): commit api-surface snapshot (9 intentional additions) + regenerated action reference doc

- api-surface: the #2874 registry exports (PUBLIC_AUTH_FEATURES et al.)
  are intentional public API - snapshot updated via gen:api-surface.
- references/ui/action.mdx: regenerated. ActionParamSchema now ends in a
  .transform() (the requiresFeature lowering), which z.toJSONSchema
  cannot represent on the output side, so the generator drops the
  ActionParam block - the same pre-existing behavior as ActionSchema and
  ObjectSchema. The authoritative ActionParam field documentation lives
  in the hand-written protocol doc
  (content/docs/protocol/objectui/actions.mdx), updated in this PR.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HGofzwNXvtahMGk8TQqXdx

---------

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation protocol:ui size/s tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant